Skip to content

fix(baileys): always emit MESSAGES_UPSERT for media even when S3 upload is skipped or fails - #2684

Open
pastoriniMatheus wants to merge 3 commits into
evolution-foundation:developfrom
pastoriniMatheus:fix/baileys-media-messages-upsert-webhook
Open

fix(baileys): always emit MESSAGES_UPSERT for media even when S3 upload is skipped or fails#2684
pastoriniMatheus wants to merge 3 commits into
evolution-foundation:developfrom
pastoriniMatheus:fix/baileys-media-messages-upsert-webhook

Conversation

@pastoriniMatheus

@pastoriniMatheus pastoriniMatheus commented Aug 12, 2026

Copy link
Copy Markdown

Problem

On the Baileys channel, media sent from the phone linked to the instance (fromMe, source = android/ios) — audio, image, document, video — does not emit the MESSAGES_UPSERT webhook when S3_ENABLED=true. The message is persisted and MESSAGES_UPDATE (status) is emitted, but the event carrying the actual content never reaches the webhook consumer. Text from the same phone works; media sent through the API works. For any consumer (CRM, chatbot, archiver) this is silent, permanent loss — no error, no retry.

Root cause — structural

src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts. The S3 upload block uses return to skip the upload, but the block sits before sendDataWebhook(Events.MESSAGES_UPSERT, ...), so skipping the upload drops the webhook with it. Webhook delivery must never depend on the outcome of the S3 upload step — that is the defect.

Concretely reachable on current develop: the video-skip branch if (isVideo && !S3.SAVE_VIDEO) return. S3.SAVE_VIDEO defaults to false (env.config.ts: process.env?.S3_SAVE_VIDEO === 'true', not set in .env.example), so it fires on every video in any S3-enabled install that didn't opt in — and the return is inside for (const received of messages), so it aborts the rest of the batch, not just the video.

Note on the exact path for the broader loss: the affected environment ran with logs suppressed (LOG_LEVEL=info), so the precise branch hit for non-video media was not instrumented and is not asserted here. The fix does not depend on it — it removes the storage-vs-delivery coupling regardless of which skip path fires.

Fix

Restructure the S3 block so it skips only the upload, never the handler — no return/continue inside the block; the webhook is always emitted. No throw for control flow; behavior unchanged when the upload succeeds. Two sites in this file share the pattern and are both fixed:

  • messages.upsert (receive path) — dropped sendDataWebhook(Events.MESSAGES_UPSERT). This is the reported case.
  • sendMessageWithTyping (API-send path) — the same if (!media) return skipped sendDataWebhook(Events.SEND_MESSAGE) and return messageRaw, so POST /message/sendMedia responded empty. Same restructure.

Impact (measured on an affected production deployment)

fromMe media delivery to the consumer went from ~7% to 100% (250/250 over 27 h), with no per-hour exceptions. A restart-vs-patch confound was ruled out: across three restarts within 11 minutes (the patch present only in the last), delivery was 0/5 on the restarts without the patch and 15/15 on the restart with it. Files remained intact in storage throughout.

Behavior note

With S3_SAVE_VIDEO=false and WEBHOOK_BASE64=true, videos that previously vanished now flow through and the base64 block embeds the full video in the webhook payload. This is the previously-dropped media now being delivered — not a regression — but installs that disabled video upload to save bandwidth should be aware. Gate the base64 block by the same SAVE_VIDEO flag if that's undesirable.

Out of scope (called out on purpose)

  • fromMe media rarely uploads to S3 at all (never gets mediaUrl) — pre-existing, not introduced here; separate issue.
  • The same class of bug exists on the Meta channel (meta/whatsapp.business.service.ts) — separate PR, not mixed into this one.

Testing

The repo has no test suite today (npm test points at a non-existent ./test/all.test.ts; the quality CI runs eslint src + tsc --noEmit + tsup only, which pass). Regression cases to lock in when a harness exists:

  • messages.upsert: with S3.ENABLE=true and S3.SAVE_VIDEO=false, a videoMessage anywhere in a batch must NOT stop sendDataWebhook(MESSAGES_UPSERT) from firing for the other messages in that batch.
  • sendMessageWithTyping: same config, POST /message/sendMedia for a video must still return the message object and emit SEND_MESSAGE (not respond empty).

…ad is skipped or fails

In the messages.upsert handler, the S3 upload block returned early from the whole
handler in two cases (video upload disabled; getBase64FromMediaMessage returns
null), aborting before sendDataWebhook(Events.MESSAGES_UPSERT). The message is
persisted but the webhook carrying its content is never emitted.

This silently drops media messages whose upload is skipped or fails — notably
fromMe media sent from another device, where getBase64FromMediaMessage cannot
fetch the file (its mediaKey belongs to that device). Measured at ~94% media loss
for fromMe messages on a production deployment (S3 enabled).

Restructure the block so it skips only the upload, never the handler, so the
webhook is always delivered regardless of the storage outcome. The inline comment
("returning early from this block") shows the original intent was to skip the
upload only; another method in the same file already uses throw for the
equivalent case.
@sourcery-ai

sourcery-ai Bot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Reviewer's Guide

Ensures the WhatsApp Baileys messages.upsert handler always emits MESSAGES_UPSERT for media messages, even when S3 upload is disabled, skipped, or fails, by restructuring the S3 upload block to avoid early returns that previously aborted the handler.

Sequence diagram for updated Baileys messages.upsert media handling

sequenceDiagram
  participant BaileysStartupService
  participant S3Service as s3Service
  participant PrismaMedia as prismaRepository_media
  participant PrismaMessage as prismaRepository_message
  participant Webhook as sendDataWebhook

  BaileysStartupService->>BaileysStartupService: messages.upsert(received)
  alt isMedia && S3.ENABLE
    alt isVideo && !S3.SAVE_VIDEO
      BaileysStartupService->>BaileysStartupService: logger.warn('Video upload is disabled. Skipping video upload.')
      note over BaileysStartupService: Skip upload only, continue handler
    else nonVideo or S3.SAVE_VIDEO
      BaileysStartupService->>BaileysStartupService: hasValidMediaContent(message)
      alt !hasRealMedia
        BaileysStartupService->>BaileysStartupService: logger.warn('Message detected as media but contains no valid media content')
      else hasRealMedia
        BaileysStartupService->>BaileysStartupService: getBase64FromMediaMessage(message, true)
        alt media is null
          BaileysStartupService->>BaileysStartupService: logger.verbose('No valid media to upload (messageContextInfo only), skipping MinIO')
          note over BaileysStartupService: No upload, continue handler
        else media available
          BaileysStartupService->>S3Service: uploadFile(fullName, buffer, size, headers)
          BaileysStartupService->>PrismaMedia: media.create(data)
          BaileysStartupService->>S3Service: getObjectUrl(fullName)
          BaileysStartupService->>PrismaMessage: message.update({ id: msg.id }, messageRaw)
        end
      end
    end
  else !isMedia or !S3.ENABLE
    BaileysStartupService->>BaileysStartupService: proceed without S3 upload
  end

  BaileysStartupService->>Webhook: sendDataWebhook(Events.MESSAGES_UPSERT, messageRaw)
Loading

File-Level Changes

Change Details Files
Restructure S3 media upload handling so webhook emission is decoupled from storage success and early returns are eliminated.
  • Remove early return paths that exited the messages.upsert handler when video upload was disabled or when media base64 extraction failed.
  • Wrap the media processing and S3 upload logic in an if/else that only skips the upload but keeps the handler running.
  • Add conditional branches to handle cases with no valid media content, failed base64 extraction, and successful uploads, updating the message record only when an upload succeeds.
  • Retain and reuse the existing media upload, metadata creation, and prisma message update flow, now guarded behind successful media retrieval.
src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 1 issue, and left some high level feedback:

  • The S3 upload block is now quite deeply nested; consider extracting the media upload and persistence logic into a separate helper to simplify the messages.upsert handler’s control flow and improve readability.
  • You are calling this.configService.get('S3') multiple times in close proximity; caching the S3 config locally within the handler would reduce repetition and make it clearer which configuration values are being used.
  • The new explanatory comment above the try block is detailed but lengthy; trimming it down or moving the deeper context into a commit message would keep the code more focused while still documenting the behavior change.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The S3 upload block is now quite deeply nested; consider extracting the media upload and persistence logic into a separate helper to simplify the messages.upsert handler’s control flow and improve readability.
- You are calling this.configService.get<S3>('S3') multiple times in close proximity; caching the S3 config locally within the handler would reduce repetition and make it clearer which configuration values are being used.
- The new explanatory comment above the try block is detailed but lengthy; trimming it down or moving the deeper context into a commit message would keep the code more focused while still documenting the behavior change.

## Individual Comments

### Comment 1
<location path="src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts" line_range="1603-1604" />
<code_context>
+                      if (!media) {
+                        this.logger.verbose('No valid media to upload (messageContextInfo only), skipping MinIO');
+                      } else {
+                        const { buffer, mediaType, fileName, size } = media;
+                        const mimetype = mimeTypes.lookup(fileName).toString();
+                        const fullName = join(
+                          `${this.instance.id}`,
</code_context>
<issue_to_address>
**issue (bug_risk):** Guard against mimeTypes.lookup returning a falsy value before calling toString.

`mimeTypes.lookup(fileName)` can return `false`/`null` for unknown types, so `.toString()` may throw and break the upload flow. Please handle the falsy case (e.g. with a default like `'application/octet-stream'` or an explicit check) before converting to string.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +1603 to +1604
const { buffer, mediaType, fileName, size } = media;
const mimetype = mimeTypes.lookup(fileName).toString();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk): Guard against mimeTypes.lookup returning a falsy value before calling toString.

mimeTypes.lookup(fileName) can return false/null for unknown types, so .toString() may throw and break the upload flow. Please handle the falsy case (e.g. with a default like 'application/octet-stream' or an explicit check) before converting to string.

…3 upload is skipped or fails

The `!media` early-return also exited the whole method, skipping sendDataWebhook(Events.SEND_MESSAGE) and `return messageRaw` — so POST /message/sendMedia responded empty. Mirror the messages.upsert fix: skip only the upload, never the method.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant